/*
 * This file is part of libsidplayfp, a SID player engine.
 *
 * Copyright 2012-2015 Leandro Nini <drfiemost@users.sourceforge.net>
 * Copyright 2007-2010 Antti Lankila
 * Copyright 2000-2001 Simon White
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#include "PSID.h"

#include <cstring>
#include <string>
#include <memory>

#include "sidplayfp/SidTuneInfo.h"

#include "sidendian.h"
#include "sidmd5.h"

namespace libsidplayfp
{

constexpr int PSID_MAXSTRLEN = 32;


// Header has been extended for 'RSID' format
// The following changes are present:
//     id = 'RSID'
//     version = 2, 3 or 4
//     play, load and speed reserved 0
//     psidspecific flag is called C64BASIC flag
//     init cannot be under ROMS/IO memory area
//     load address cannot be less than $07E8
//     info strings may be 32 characters long without trailing zero

struct psidHeader           // all values are big-endian
{
    uint32_t id;                   // 'PSID' or 'RSID' (ASCII)
    uint16_t version;              // 1, 2, 3 or 4
    uint16_t data;                 // 16-bit offset to binary data in file
    uint16_t load;                 // 16-bit C64 address to load file to
    uint16_t init;                 // 16-bit C64 address of init subroutine
    uint16_t play;                 // 16-bit C64 address of play subroutine
    uint16_t songs;                // number of songs
    uint16_t start;                // start song out of [1..256]
    uint32_t speed;                // 32-bit speed info
                                   // bit: 0=50 Hz, 1=CIA 1 Timer A (default: 60 Hz)
    char name[PSID_MAXSTRLEN];     // ASCII strings, 31 characters long and
    char author[PSID_MAXSTRLEN];   // terminated by a trailing zero
    char released[PSID_MAXSTRLEN]; //

    uint16_t flags;                // only version >= 2
    uint8_t relocStartPage;        // only version >= 2ng
    uint8_t relocPages;            // only version >= 2ng
    uint8_t sidChipBase2;          // only version >= 3
    uint8_t sidChipBase3;          // only version >= 4
};

enum
{
    PSID_MUS       = 1 << 0,
    PSID_SPECIFIC  = 1 << 1, // These two are mutally exclusive
    PSID_BASIC     = 1 << 1,
    PSID_CLOCK     = 3 << 2,
    PSID_SIDMODEL  = 3 << 4
};

enum
{
    PSID_CLOCK_UNKNOWN = 0,
    PSID_CLOCK_PAL     = 1 << 2,
    PSID_CLOCK_NTSC    = 1 << 3,
    PSID_CLOCK_ANY     = PSID_CLOCK_PAL | PSID_CLOCK_NTSC
};

enum
{
    PSID_SIDMODEL_UNKNOWN = 0,
    PSID_SIDMODEL_6581    = 1,
    PSID_SIDMODEL_8580    = 2,
    PSID_SIDMODEL_ANY     = PSID_SIDMODEL_6581 | PSID_SIDMODEL_8580
};

// Format strings
const char TXT_FORMAT_PSID[]  = "PlaySID one-file format (PSID)";
const char TXT_FORMAT_RSID[]  = "Real C64 one-file format (RSID)";
const char TXT_UNKNOWN_PSID[] = "Unsupported PSID version";
const char TXT_UNKNOWN_RSID[] = "Unsupported RSID version";

constexpr int psid_headerSize = 118;
constexpr int psidv2_headerSize = psid_headerSize + 6;

// Magic fields
constexpr uint32_t PSID_ID = 0x50534944;
constexpr uint32_t RSID_ID = 0x52534944;

/**
 * Decode SID model flags.
 */
SidTuneInfo::model_t getSidModel(uint_least16_t modelFlag)
{
    if ((modelFlag & PSID_SIDMODEL_ANY) == PSID_SIDMODEL_ANY)
        return SidTuneInfo::SIDMODEL_ANY;

    if (modelFlag & PSID_SIDMODEL_6581)
        return SidTuneInfo::SIDMODEL_6581;

    if (modelFlag & PSID_SIDMODEL_8580)
        return SidTuneInfo::SIDMODEL_8580;

    return SidTuneInfo::SIDMODEL_UNKNOWN;
}

/**
 * Check if extra SID addres is valid for PSID specs.
 */
bool validateAddress(uint_least8_t address)
{
    // Only even values are valid.
    if (address & 1)
        return false;

    // Ranges $00-$41 ($D000-$D410) and $80-$DF ($D800-$DDF0) are invalid.
    // Any invalid value means that no second SID is used, like $00.
    if (address <= 0x41
            || (address >= 0x80 && address <= 0xdf))
        return false;

    return true;
}

SidTuneBase* PSID::load(buffer_t& dataBuf)
{
    // File format check
    if (dataBuf.size() < 4)
    {
        return nullptr;
    }

    const uint32_t magic = endian_big32(&dataBuf[0]);
    if ((magic != PSID_ID)
        && (magic != RSID_ID))
    {
        return nullptr;
    }

    psidHeader pHeader;
    readHeader(dataBuf, pHeader);

    std::unique_ptr<PSID> tune(new PSID());
    tune->tryLoad(pHeader);

    return tune.release();
}

void PSID::readHeader(const buffer_t &dataBuf, psidHeader &hdr)
{
    // Due to security concerns, input must be at least as long as version 1
    // header plus 16-bit C64 load address. That is the area which will be
    // accessed.
    if (dataBuf.size() < (psid_headerSize + 2))
    {
        throw loadError(ERR_TRUNCATED);
    }

    // Read v1 fields
    hdr.id               = endian_big32(&dataBuf[0]);
    hdr.version          = endian_big16(&dataBuf[4]);
    hdr.data             = endian_big16(&dataBuf[6]);
    hdr.load             = endian_big16(&dataBuf[8]);
    hdr.init             = endian_big16(&dataBuf[10]);
    hdr.play             = endian_big16(&dataBuf[12]);
    hdr.songs            = endian_big16(&dataBuf[14]);
    hdr.start            = endian_big16(&dataBuf[16]);
    hdr.speed            = endian_big32(&dataBuf[18]);

    std::memcpy(hdr.name,     &dataBuf[22], PSID_MAXSTRLEN);
    std::memcpy(hdr.author,   &dataBuf[54], PSID_MAXSTRLEN);
    std::memcpy(hdr.released, &dataBuf[86], PSID_MAXSTRLEN);

    if (hdr.version >= 2)
    {
        if (dataBuf.size() < (psidv2_headerSize + 2))
        {
            throw loadError(ERR_TRUNCATED);
        }

        // Read v2/3/4 fields
        hdr.flags            = endian_big16(&dataBuf[118]);
        hdr.relocStartPage   = dataBuf[120];
        hdr.relocPages       = dataBuf[121];
        hdr.sidChipBase2     = dataBuf[122];
        hdr.sidChipBase3     = dataBuf[123];
    }
}

void PSID::tryLoad(const psidHeader &pHeader)
{
    SidTuneInfo::compatibility_t compatibility = SidTuneInfo::COMPATIBILITY_C64;

    // Require a valid ID and version number.
    if (pHeader.id == PSID_ID)
    {
       switch (pHeader.version)
       {
       case 1:
           compatibility = SidTuneInfo::COMPATIBILITY_PSID;
           break;
       case 2:
       case 3:
       case 4:
           break;
       default:
           throw loadError(TXT_UNKNOWN_PSID);
       }
       info->m_formatString = TXT_FORMAT_PSID;
    }
    else if (pHeader.id == RSID_ID)
    {
       switch (pHeader.version)
       {
       case 2:
       case 3:
       case 4:
           break;
       default:
           throw loadError(TXT_UNKNOWN_RSID);
       }
       info->m_formatString = TXT_FORMAT_RSID;
       compatibility = SidTuneInfo::COMPATIBILITY_R64;
    }

    fileOffset             = pHeader.data;
    info->m_loadAddr       = pHeader.load;
    info->m_initAddr       = pHeader.init;
    info->m_playAddr       = pHeader.play;
    info->m_songs          = pHeader.songs;
    info->m_startSong      = pHeader.start;
    info->m_compatibility  = compatibility;
    info->m_relocPages     = 0;
    info->m_relocStartPage = 0;

    uint_least32_t speed = pHeader.speed;
    SidTuneInfo::clock_t clock = SidTuneInfo::CLOCK_UNKNOWN;

    bool musPlayer = false;

    if (pHeader.version >= 2)
    {
        const uint_least16_t flags = pHeader.flags;

        // Check clock
        if (flags & PSID_MUS)
        {   // MUS tunes run at any speed
            clock = SidTuneInfo::CLOCK_ANY;
            musPlayer = true;
        }
        else
        {
            switch (flags & PSID_CLOCK)
            {
            case PSID_CLOCK_ANY:
                clock = SidTuneInfo::CLOCK_ANY;
                break;
            case PSID_CLOCK_PAL:
                clock = SidTuneInfo::CLOCK_PAL;
                break;
            case PSID_CLOCK_NTSC:
                clock = SidTuneInfo::CLOCK_NTSC;
                break;
            default:
                break;
            }
        }

        // These flags are only available for the appropriate
        // file formats
        switch (compatibility)
        {
        case SidTuneInfo::COMPATIBILITY_C64:
            if (flags & PSID_SPECIFIC)
                info->m_compatibility = SidTuneInfo::COMPATIBILITY_PSID;
            break;
        case SidTuneInfo::COMPATIBILITY_R64:
            if (flags & PSID_BASIC)
                info->m_compatibility = SidTuneInfo::COMPATIBILITY_BASIC;
            break;
        default:
            break;
        }

        info->m_clockSpeed = clock;

        info->m_sidModels[0] = getSidModel(flags >> 4);

        info->m_relocStartPage = pHeader.relocStartPage;
        info->m_relocPages     = pHeader.relocPages;

        if (pHeader.version >= 3)
        {
            if (validateAddress(pHeader.sidChipBase2))
            {
                info->m_sidChipAddresses.push_back(0xd000 | (pHeader.sidChipBase2 << 4));

                info->m_sidModels.push_back(getSidModel(flags >> 6));
            }

            if (pHeader.version >= 4)
            {
                if (pHeader.sidChipBase3 != pHeader.sidChipBase2
                    && validateAddress(pHeader.sidChipBase3))
                {
                    info->m_sidChipAddresses.push_back(0xd000 | (pHeader.sidChipBase3 << 4));

                    info->m_sidModels.push_back(getSidModel(flags >> 8));
                }
            }
        }
    }

    // Check reserved fields to force real c64 compliance
    // as required by the RSID specification
    if (compatibility == SidTuneInfo::COMPATIBILITY_R64)
    {
        if ((info->m_loadAddr != 0)
            || (info->m_playAddr != 0)
            || (speed != 0))
        {
            throw loadError(ERR_INVALID);
        }

        // Real C64 tunes appear as CIA
        speed = ~0;
    }

    // Create the speed/clock setting table.
    convertOldStyleSpeedToTables(speed, clock);

    // Copy info strings.
    info->m_infoString.push_back(std::string(pHeader.name, PSID_MAXSTRLEN));
    info->m_infoString.push_back(std::string(pHeader.author, PSID_MAXSTRLEN));
    info->m_infoString.push_back(std::string(pHeader.released, PSID_MAXSTRLEN));

    if (musPlayer)
        throw loadError("Compute!'s Sidplayer MUS data is not supported yet"); // TODO
}

const char *PSID::createMD5(char *md5)
{
    if (md5 == nullptr)
        md5 = m_md5;

    *md5 = '\0';

    try
    {
        // Include C64 data.
        sidmd5 myMD5;
        myMD5.append(&cache[fileOffset], info->m_c64dataLen);

        uint8_t tmp[2];
        // Include INIT and PLAY address.
        endian_little16(tmp, info->m_initAddr);
        myMD5.append(tmp, sizeof(tmp));
        endian_little16(tmp, info->m_playAddr);
        myMD5.append(tmp, sizeof(tmp));

        // Include number of songs.
        endian_little16(tmp, info->m_songs);
        myMD5.append(tmp, sizeof(tmp));

        {
            // Include song speed for each song.
            const unsigned int currentSong = info->m_currentSong;
            for (unsigned int s = 1; s <= info->m_songs; s++)
            {
                selectSong(s);
                const uint8_t songSpeed = static_cast<uint8_t>(info->m_songSpeed);
                myMD5.append(&songSpeed, sizeof(songSpeed));
            }
            // Restore old song
            selectSong(currentSong);
        }

        // Deal with PSID v2NG clock speed flags: Let only NTSC
        // clock speed change the MD5 fingerprint. That way the
        // fingerprint of a PAL-speed sidtune in PSID v1, v2, and
        // PSID v2NG format is the same.
        if (info->m_clockSpeed == SidTuneInfo::CLOCK_NTSC)
        {
            const uint8_t ntsc_val = 2;
            myMD5.append(&ntsc_val, sizeof(ntsc_val));
        }

        // NB! If the fingerprint is used as an index into a
        // song-lengths database or cache, modify above code to
        // allow for PSID v2NG files which have clock speed set to
        // SIDTUNE_CLOCK_ANY. If the SID player program fully
        // supports the SIDTUNE_CLOCK_ANY setting, a sidtune could
        // either create two different fingerprints depending on
        // the clock speed chosen by the player, or there could be
        // two different values stored in the database/cache.

        myMD5.finish();

        // Get fingerprint.
        myMD5.getDigest().copy(md5, SidTune::MD5_LENGTH);
        md5[SidTune::MD5_LENGTH] = '\0';
    }
    catch (md5Error const &)
    {
        return nullptr;
    }

    return md5;
}

const char *PSID::createMD5New(char *md5)
{
    if (md5 == nullptr)
        md5 = m_md5;

    *md5 = '\0';

    try
    {
        // The calculation is now simplified
        // All the header + all the data
        sidmd5 myMD5;
        myMD5.append(&cache[0], cache.size());

        myMD5.finish();

        // Get fingerprint.
        myMD5.getDigest().copy(md5, SidTune::MD5_LENGTH);
        md5[SidTune::MD5_LENGTH] = '\0';
    }
    catch (md5Error const &)
    {
        return nullptr;
    }

    return md5;
}

}
